home *** CD-ROM | disk | FTP | other *** search
/ SuperHack / SuperHack CD.bin / CODING / GRAPHICS / VGATUT2.ZIP / TUT2.TXT < prev    next >
Encoding:
Text File  |  1995-08-12  |  19.5 KB  |  549 lines

  1.             ╒═════════════════════════╕
  2.             │░░░▓░░░▓░░░▓▓▓░░░░░▓░░░░░│
  3.             │░░░▓░░░▓░░▓░░░░░░░▓░▓░░░░│
  4.             │░░░░▓░▓░░░▓░░▓▓░░▓░░░▓░░░│
  5.             │░░░░▓░▓░░░▓░░░▓░░▓▓▓▓▓░░░│
  6.             │░░░░░▓░░░░░▓▓▓░░░▓░░░▓░░░│
  7.             ├═════════════════════════┤
  8.             │Part II - LINES & CIRCLES│
  9.             └─────────────────────────┘
  10.  
  11. Hi and welcome to the issue number 2 of my (ermm.. a bit late) weekly tutorial 
  12. on programming graphics for the PC.
  13.  
  14. Firstly, *many* thanks to Richard Griffiths for porting the code in this
  15. tutorial to Pascal.
  16.  
  17. This tutorial is about lines, circles & how to draw them.  
  18.  
  19. Before we start :
  20.  
  21. ═══════════════════════════════════════════════════════════════════════════════
  22. ┌══════════╗
  23. │DISCLAIMER║
  24. └──────────╜
  25.     The code for this tutorial, the compiled .EXEs and any ascociated text
  26.     are freely distributable on the conditions that the contents of the 
  27.     original .ZIP file are distributed as one and that no changes are made
  28.     to any of the data or information contained within.
  29.     The author expresses no warranty implied or otherwise as to the
  30.     suitabilty of this software for execution on any machine other than
  31.     the system on which it was developed (although there should be no
  32.     problems).
  33.     The author also takes no responsibilty for damage resulting from the
  34.     use of information, code or otherwise, obtained from this tutorial 
  35.     (again, I'd be surprised if such a thing did happen).
  36.     Finally you should _NOT_ have paid anything for this tutorial.  
  37.     If you parted with any money (other than a reasonalbe price for a 
  38.     disk) then complain and get your money back.  Please let me know too. 
  39.     This tutorial is FREE.  Please do not abuse this.
  40. ═══════════════════════════════════════════════════════════════════════════════
  41.  
  42. Sorry about all that, but unfortunately we live in a world where such things
  43. are a necessity.
  44.  
  45. Time is short, so lets press on.......
  46.  
  47. Drawing a line.  What could be simpler?  Well, actually lots of things.  Like
  48. riding a bike, brushing your teeth, walking the dog..... no, seriously though
  49.  - the problems involved with drawing a line on a VGA screen are not trivial.
  50.  
  51. LINES (a brief revision)
  52. ────────────────────────
  53.  
  54. We can describe a line in several ways.  Look at the following....
  55.  
  56.     y = mx + c
  57.  
  58. Familliar?  You bet!  This is a standard equation for a line which you should
  59. have learned in maths at school.  What it basically says is, Y is equal to X
  60. multiplied by the gradient of the line (m) plus the Y-intercept (the value of
  61. Y when X = 0, or effectively where it crosses the Y axis).
  62.  
  63. If we know the gradient of the line, and the point at which it crosses the Y
  64. axis, then we can draw a line of infinate length which fits the equation.
  65. All we need to do is start with X=0 (or -100, or 56.73, or 999, you get the 
  66. idea) and using that equation, we can calculate the value of the corresponding
  67. Y coordinate.
  68.  
  69. How do we find the gradient of the line?
  70. ────────────────────────────────────────
  71.  
  72. The following formula requires us to know at least two points on the line,
  73. which we can then use to calculate the gradient.
  74.  
  75.     m = (x2-x1)/(y2-y1)
  76.  
  77.     The points (100,200) & (56, 37) (using the above formula) would give us
  78.     a gradient of 0.27.
  79.  
  80.     This means that for every change of 1 unit in the X direction, there is a
  81.     change of 0.27 units in the Y direction.  So the first few points of our
  82.     line would look like this.... (assuming a Y intercept of 0)
  83.  
  84.     X │ Y
  85.     ──┼─────
  86.     0 │ 0
  87.     1 │ 0.27
  88.     2 │ 0.54
  89.     3 │ 0.81
  90.     4 │ 1.08       
  91.  
  92.     As those of you who were paying attention last time will know, we cannot
  93.     plot fractions on the screen and therefore, this approach will need quite
  94.     a bit of change.
  95.  
  96. What exactly do we want to do?
  97. ──────────────────────────────
  98.  
  99. Instead of using that formula we will write a function which takes the
  100. start and end points of the line and draws the pixels in between.
  101.  
  102. If you look at a line on a monitor screen, you will see that it is far from
  103. being a perfect line.  It looks more like this....
  104.  
  105.         OOOOO
  106.              OOOOO
  107.               OOOOO
  108.                    OOOOO
  109.  
  110. The distance and the overall gradient of this line are as expected, but
  111. because of the fact that we do not have the freedom to play with fractions of 
  112. pixels, we must approximate each calculation to the nearest pixel.
  113.  
  114. How do we do this?
  115.  
  116. Firstly we need to ensure that the point with the highest value (ie. further 
  117. down the screen) appears as X2.  Rather than rely on the parameters being
  118. passed in this order, which limits our function greatly, we will swap them
  119. around if necessary...
  120.     
  121.     if (X2<X1)
  122.     {
  123.     int Tmp = X2;
  124.     X2 = X1;
  125.     X1 = Tmp;
  126.  
  127.     Tmp = Y2;
  128.     Y2 = Y1;
  129.     Y1 = Tmp;
  130.     }
  131.  
  132. Good.  Now the coordinates are in the right order.
  133.  
  134. The next thing we need to know is weather we a drawing a horizontal or
  135. verticle line.  These two types of line are the simplest to deal with,
  136. because only one value is changed.  If the line is horizontal the gradient
  137. is zero (no change in Y) and if it is vertical, them the gradient is infinate
  138. (an infinite number of Y values for 1 value of X).  So we make sure that we
  139. deal with these two types of line differently.
  140.  
  141.        if(Y2 != Y1) &&
  142.      (X2 != X1)
  143.        )
  144.  
  145. Once we have determined that we are drawing a sloped line we have to 
  146. calculate the gradient.
  147.  
  148.     Slope = (float)(Y2-Y1)/(X2-X1); // gradient of line
  149.  
  150. Now all that we need to do is begin at the first point (X1, Y1) and
  151. step along the line from X1 to X2 with the following loop....
  152.  
  153.         for (int Temp=X1; Temp<=X2; Temp++)
  154.         {
  155.         PutPixel( Temp, (int)YPos, Colour );
  156.         YPos += Slope;
  157.         }
  158.  
  159. Here we are adding the value of Slope to the old Y coordination and plotting
  160. the point.  Note that the value of YPos is still a floating point value and
  161. we are 'kludging' the value to an integer by passing it as a variable to a
  162. function which requires an integer value, effectively stripping the decimal
  163. portion from the number (ie. 10.1231 becomes 10)
  164.  
  165. This mini-function will work fine for lines where the slope is greater than
  166. 0 and less than 1.  After that point we begin to have problems....
  167.  
  168. What happens is that when slope is greater than 1, we are adding too much
  169. to the value of Y, so the line begins to break up.
  170.  
  171. To deal with this, we split the calculation into two pieces.  The first for
  172. lines of slope less than 1, and well.. the other one... obviously.
  173.  
  174. So the line drawing looks like this :
  175.  
  176.     if (Slope<1)
  177.     {
  178.         YPos = Y1;
  179.         
  180.         for (int Temp=X1; Temp<=X2; Temp++)
  181.         {
  182.         PutPixel( Temp, (int)YPos, Colour );
  183.         YPos += Slope;
  184.         }
  185.     }
  186.     else
  187.     {
  188.         XPos = X1;
  189.  
  190.         for (int Temp=Y1; Temp<=Y2; Temp++)
  191.         {
  192.         PutPixel( (int)XPos, Temp, Colour );
  193.         XPos += (1/Slope);
  194.         }
  195.     }
  196.     }
  197.     
  198. Now all that's left is to deal with horizontal or verticle lines...
  199.  
  200.     else   // from the line - if (Y2 != Y1) &&
  201.     {
  202.     if (X1 == X2)   // verticle
  203.     {
  204.         for (int Temp=Y1; Temp<=Y2; Temp++)
  205.         PutPixel( X1, Temp, Colour );
  206.     }
  207.     else    // horizontal
  208.     {
  209.         for (int Temp=X1; Temp<=X2; Temp++)
  210.         PutPixel( Temp, Y1, Colour );
  211.     }
  212.     }
  213.  
  214. And there we have it!  The full routine is included with this text.
  215.  
  216. Slow isn't it?  Using floating point arithmetic is always a killer when
  217. trying to write fast routines.  What we need to do is remove all the
  218. floating point.  Also the repeated calling of the PutPixel function slows
  219. things down a great deal, so we need another *new* way of putting a pixel on
  220. screen.
  221.  
  222. As it's the gradient which requires the use of floating point math, we
  223. need a way of drawing the line without the need for calculating a gradient.
  224. Fortunately for us, this problem was solved a long time ago by a chap by
  225. the name of Bresenham.  His famous algorithm for drawing lines is very fast
  226. and used widely in many line routines.
  227.  
  228. The first thing that we need to do is calculate the range of the line.  That
  229. is, the difference between the starting and the ending coordinates.
  230.  
  231.     XScope = (X2 - X1);
  232.     YScope = (Y2 - Y1);
  233.  
  234. The values XScope & YScope could easily end up negative and we must have them
  235. positive so we do the following....
  236.  
  237.     if (XScope < 0)
  238.     {
  239.     XScope =- XScope;   // get absolute value of X term
  240.     XDir = -1;          // direction of line
  241.     }
  242.     else XDir = 1;          // positive
  243.  
  244.     if (YScope < 0)
  245.     {
  246.     YScope =- YScope;   // get absoulte value of Y term
  247.     YDir = -320;        // familiar?
  248.     }
  249.     else YDir = 320;
  250.  
  251. As you will remember from the previous tutorial, we declared a variable 
  252. which was used to point at video memory.  This variable is a pointer which
  253. we can also treat as an array of 64000 unsigned chars, and when we place a
  254. value in this array it appears on screen.
  255.  
  256. Therefore the statement :               vga[0] = 15 
  257. will place a white pixel at (0,0)
  258.  
  259. This is the method that we will use in our line routine to avoid having to
  260. call the Setpixel function repeatedly.
  261.  
  262.     Offset = X1 + (Y1*320); // video offset of first point
  263.  
  264. That's the stage set, now for the interesting bit....
  265.  
  266.     LinearDeviance = 0;
  267.  
  268. This variable is the mainstay of the whole function.  The first thing we do is
  269. (as in the previous function), work out which way the line is running.
  270.  
  271.     if (XScope > YScope)
  272.     
  273. We then set the length of the line equal based on the success of the IF test.
  274.  
  275.     Length = XScope+1;  // success
  276.     Length = YScope+1;  // fail
  277.  
  278. This means, obviously, that the length that the line is calculated over is
  279. equal to the greater of the two differences.
  280.  
  281. We then begin our line drawing loop...
  282.  
  283.     for (int Tmp=0; Tmp <= Length; Tmp++)
  284.     {
  285.         vga[Offset] = Colour;       // put pixel
  286.  
  287.         LinearDeviance += YScope;
  288.  
  289.         if (LinearDeviance >= XScope)    // error threshold has been exceeded
  290.         {
  291.         LinearDeviance -= XScope;
  292.         Offset += YDir;
  293.         }
  294.  
  295.         Offset += XDir;
  296.     }
  297.  
  298. Here we step though each point on the line, repeatedly adding YScope to
  299. LinearDeviance.  When the deviance of the line (that is the difference between
  300. the pixelated line & the mathematically desired line) becomes greater than
  301. XScope, we subtract XScope and adjust our Y coordinate appropriately.
  302.  
  303.  
  304. Now all that's left to do is deal with YScope being greater than XScope...
  305.  
  306.     {
  307.     Length = YScope+1;
  308.  
  309.     for (int Tmp=0; Tmp <= Length; Tmp++)
  310.     {
  311.         vga[Offset] = Colour;       // put pixel
  312.  
  313.         LinearDeviance += XScope;
  314.  
  315.         if (LinearDeviance >= YScope)    // error threshold has been exceeded
  316.         {
  317.         LinearDeviance -= YScope;
  318.         Offset += XDir;
  319.         }
  320.  
  321.         Offset += YDir;
  322.     }
  323.     }
  324.  
  325. And that's it!  Apologies for not delving into this as deeply as I would have
  326. liked but that old swinger 'Father Time' has gotten ahead of me again & I
  327. still have to cover circles....  so let's press on.
  328.  
  329. By the way, both of these techniques are demonstrated in the programs
  330. accompanying this tutorial.
  331.  
  332. ──────────────────────────────────────────────────────────────────────────────
  333. THE CIRCLE
  334. ──────────
  335.  
  336. We're all familiar with the good old circle.  It is described by three 
  337. values : the coordinates of the centre point & the radius.
  338.  
  339. What we need is a way of generating a circle based on these values.
  340.  
  341. The function declaration will look like this :
  342.  
  343.     DrawCircle( int X, int Y, int Radius )
  344.  
  345.  
  346. Drawing a Circle
  347. ────────────────
  348.  
  349. In order to draw a circle we must be able to generate integer X & Y 
  350. coordinates for each point on the circumference.  The same problems that we
  351. have with drawing a line with integer values also occur when drawing a circle,
  352. only more seriously....
  353.  
  354. A circle can be described with the following equations....
  355.  
  356.     X = Cos(Θ)
  357.     Y = Sin(Θ)
  358.  
  359. Which is great because it provides the data in the exact format that we want,
  360. a coordinate pair.
  361.  
  362. It is therefore a simple matter to place this, and the PutPixel function
  363. inside a loop which moves through theta (Θ) from 0 to 360 generating the 
  364. circle.
  365.  
  366. Simple!  No.  :(
  367.  
  368. There are a few things that we must do before this will produce the circle
  369. that we need.
  370.  
  371. Firstly : The circle will be generated with a radius of 1, at location (0,0),
  372.       which is not in keeping with the specification that we designed
  373.       earlier.
  374.  
  375. Secondly : The Sin & Cos functions provided by C++ compilers, deal with
  376.        radians, rather than degrees, so we must first tailor our loop to
  377.        deal with this.
  378.  
  379. Ok.  Moving swiftly onward, we can see that there are some transformations we
  380. can do on our X, Y values to produce the required circle.
  381.  
  382. We must scale the original circle by the radius, and move it to the desired
  383. location.  Like this....
  384.  
  385.        PointX = (Cos(Θ) * Radius) + X;  // X is the X coodinate of the centre
  386.        PointY = (Sin(Θ) * Radius) + Y;
  387.  
  388. This will move our circle from (0,0) to the correct place & scale it to the
  389. appropriate radius at the same time.
  390.  
  391. Now we plot the point with our PutPixel.
  392.  
  393. And that's the first problem sorted.
  394.  
  395. As far as the second problem is concerned, all we need to do is create a loop
  396. like this:
  397.  
  398.     for (float Tmp=0; Tmp<6.3; Tmp += 0.005)
  399.  
  400.  
  401. 6.283 radians = 360 degrees
  402.  
  403. We increment Tmp by 0.005 each loop in order to hit a reasonable sample of
  404. angles.  This is probably a bit overkill (31500 samples) but we'll deal with
  405. that later on.  For now, realise that by increasing this value we speed the
  406. routine up, but also reduce the quality of the circle.
  407.  
  408. And that's it.  A simple circle drawing routine.
  409.  
  410. A word of warning.  It's rubbish.  Don't use it. <grin>
  411. When the circles are small they look more like sqaures & when they increase
  412. in size they have holes in them (need to increase sample rate).
  413. Also the function is terribly slow.
  414.  
  415. A Better Routine
  416. ────────────────
  417.  
  418. There are a number of things that we can do to improve the previous routine.
  419.  
  420. Ideally, we need to remove the use floating point numbers, as we did for the
  421. line routine (above), but this is not very easy when dealing with Sine & 
  422. Cosine functions as they will return floating point values.  I'll work on it
  423. and let you know if I come up with anything better.
  424.  
  425. The best method of improving the accuracy of the circle is to modify the
  426. sample rate based on the size.  We need a sample rate which provides enough
  427. samples to hit all the pixels required, but not too many so as to slow the
  428. routine down unnecesarily.  I've used the following formula :
  429.  
  430.     Sample = 1/Radius
  431.  
  432. This is simply an inverse function which provides a higher sample rate for
  433. larger circles and reduces it for small circles.
  434.  
  435. Ok.  That's accuracy dealt with.... now for the speed.
  436.  
  437. I'm tempted to do this in assembly, but I'll stick with C++ code for this
  438. tutorial for simplicity's sake.
  439.  
  440. Again, rather than use the PutPixel function, I'll use the vga[] array which
  441. we used before, but this time we need to calculate the offset each time rather
  442. than incrementing.  Consider the formula :  (Y*320)+X
  443.  
  444. By using the SHL operator in assembly code, we managed to speed our PutPixel,
  445. we can use the same trick from C++ code too....
  446.  
  447.     vga[ (Y<<6)+(Y<<8)+X ] = Colour
  448.  
  449. The Shift left operator '<<' is used to perform the same operation as the
  450. SHL assembly command.
  451.  
  452. Now... one last trick.  We will make use of the circle's most obvious 
  453. feature.... it's symmetry.
  454.  
  455. For each point on the circle that we calculate we can mirror it at several
  456. points around the circle.  As the circle has infinate lines of symettry, we
  457. can also have an infinate number of copies (although this is silly) of each
  458. calculated pixel.
  459.  
  460. We will deal with 2 lines of symetry (you may have guessed, but I don't know
  461. how to spell symettry), meaning that for every point we calculate we repeat
  462. this point at 3 other places around the circle.  This cuts the number of
  463. calculations required to 1/4 of the number used in the previous function! :)
  464.  
  465. A slight change to the equations for calculating the points are required
  466. here.  We must remove the +X & +Y that we use to position the circle and add
  467. them later on.  They now look like this.....
  468.  
  469.  
  470.        PointX = (Cos(Θ) * Radius);
  471.        PointY = (Sin(Θ) * Radius);
  472.  
  473. We repeat the point like this......
  474.  
  475.     NewX1 = NewX + X;         // calculated point + transformation
  476.     NewY1 = NewY + Y;
  477.  
  478.     NewX2 = (NewX * -1) + X;
  479.     NewY2 = (NewY * -1) + Y;
  480.  
  481.     NewX3 = (NewX * -1) + X;
  482.     NewY3 =  NewY + Y;
  483.  
  484.     NewX4 =  NewX + X;
  485.     NewY4 = (NewY * -1) + Y;
  486.  
  487.     screen[( (NewY1<<8) + (NewY1<<6) + NewX1 )] = Colour;
  488.     screen[( (NewY2<<8) + (NewY2<<6) + NewX2 )] = Colour;
  489.     screen[( (NewY3<<8) + (NewY3<<6) + NewX3 )] = Colour;
  490.     screen[( (NewY4<<8) + (NewY4<<6) + NewX4 )] = Colour;
  491.  
  492. We calculate X & Y using the equations derived earlier and apply 
  493. transformations that allow us to position them in the correct places.
  494.  
  495. This technique can be extended to do 4 lines of reflection (8 repetitions) or
  496. even more, though this introduces floating point calculations & perhaps undo
  497. the advantages.  I'll leave you to experiment.
  498.  
  499. Goodbye....
  500. ───────────
  501.  
  502. You'll notice that there are a number of source files included with this
  503. Tut.  VGAFUNC.CPP & VGAFUNC.H are my attempt to keep all the best routines
  504. that we could use again, in a single place which can be compiled along with
  505. any new code and save us rewriting ancient routines, over & over again.
  506. At the end of each tutorial then it would be a prudent move to copy the
  507. routines that you like from the original source and move them to this file.
  508. Note that the crappy routines from this Tut are also in this file, and don't
  509. want to be so move them to another file if you want to keep them (heaven
  510. knows why would want to do that?!!)
  511.  
  512. Next week I'll be looking at the VGA Palette and I'll be providing several
  513. routines to help you do nice things with colours.  I've actually written the
  514. code already, but not the text so you'll have to wait until I have time for
  515. that. ;)
  516.  
  517. (Standard bit ripped from last tutorial)──────────┐
  518. ┌─────────────────────────────────────────────────┴─────────────────────────┐
  519. │If there is anything in this tutorial which you would like further details │
  520. │on, then please do not hesitate to contact me.                             │
  521. │                                                                           │
  522. │If you produce anything nice, then please send it to me.  I am always keen │
  523. │to see other people's work.                                                │
  524. │                                                                           │
  525. │Comments on this tutorial will be gratefully recieved.  Was it any use to  │
  526. │you? Too informal?  Too complicated?  Not clear enough?  Too simple?       │
  527. │What subjects would you like to see covered in later editions?             │
  528. └───────────────────────────────────────────────────────────────────────────┘
  529. ┌────┤From 'The Wall' by Pink Floyd├────────────────────────────────────────┐
  530. │                                                                           │
  531. │'Did you see the frightened ones?                                          │
  532. │ Did you hear the falling bombs?                                           │
  533. │ Did you ever wonder why we had to run for shelter,                        │
  534. │ with the promise of a brave new world,                                    │
  535. │ unfurled beneath a clear blue sky?                                        │
  536. │'                                                                          │
  537. └───────────────────────────────────────────────────────────────────────────┘
  538.  
  539. Barny Mercer    - (original code & text file) 5/8/95 @ 9:25pm
  540.  
  541. email : barny.mercer@zetnet.co.uk
  542. WWW   : http://www.zetnet.co.uk/users/bmercer/
  543. voice : 01595 692097
  544.  
  545. Richard Griffiths       - Pascal conversion
  546. email : richard.griffiths@zetnet.co.uk
  547. WWW   : http://www.zetnet.co.uk/users/rgriff/
  548.     
  549.